/*
 * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available.
 *
 * Copyright (C) 2021 THL A29 Limited, a Tencent company.  All rights reserved.
 *
 * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license.
 *
 * A copy of the MIT License is included in this file.
 *
 *
 * Terms of the MIT License:
 * ---------------------------------------------------
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
 * the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
 * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.tencent.bkrepo.repository.service.repo.impl

import com.tencent.bkrepo.auth.api.ServiceBkiamV3ResourceClient
import com.tencent.bkrepo.auth.api.ServicePermissionClient
import com.tencent.bkrepo.common.api.constant.DEFAULT_PAGE_NUMBER
import com.tencent.bkrepo.common.api.constant.TOTAL_RECORDS_INFINITY
import com.tencent.bkrepo.common.api.constant.DEFAULT_PAGE_SIZE
import com.tencent.bkrepo.common.api.constant.CLOSED_SOURCE_PREFIX
import com.tencent.bkrepo.common.api.constant.CODE_PROJECT_PREFIX
import com.tencent.bkrepo.common.api.exception.ErrorCodeException
import com.tencent.bkrepo.common.api.message.CommonMessageCode
import com.tencent.bkrepo.common.api.pojo.Page
import com.tencent.bkrepo.common.api.util.EscapeUtils
import com.tencent.bkrepo.common.api.util.Preconditions
import com.tencent.bkrepo.common.artifact.message.ArtifactMessageCode
import com.tencent.bkrepo.common.mongo.dao.util.Pages
import com.tencent.bkrepo.common.service.cluster.DefaultCondition
import com.tencent.bkrepo.repository.dao.ProjectDao
import com.tencent.bkrepo.repository.dao.repository.ProjectMetricsRepository
import com.tencent.bkrepo.repository.listener.ResourcePermissionListener
import com.tencent.bkrepo.repository.model.TProject
import com.tencent.bkrepo.repository.model.TProjectMetrics
import com.tencent.bkrepo.repository.pojo.project.ProjectCreateRequest
import com.tencent.bkrepo.repository.pojo.project.ProjectInfo
import com.tencent.bkrepo.repository.pojo.project.ProjectListOption
import com.tencent.bkrepo.repository.pojo.project.ProjectMetadata
import com.tencent.bkrepo.repository.pojo.project.ProjectMetricsInfo
import com.tencent.bkrepo.repository.pojo.project.ProjectRangeQueryRequest
import com.tencent.bkrepo.repository.pojo.project.ProjectSearchOption
import com.tencent.bkrepo.repository.pojo.project.ProjectUpdateRequest
import com.tencent.bkrepo.repository.service.repo.ProjectService
import com.tencent.bkrepo.repository.service.repo.StorageCredentialService
import com.tencent.bkrepo.repository.util.ProjectEventFactory.buildCreatedEvent
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Conditional
import org.springframework.context.annotation.Lazy
import org.springframework.dao.DuplicateKeyException
import org.springframework.data.domain.Sort
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.data.mongodb.core.query.Query
import org.springframework.data.mongodb.core.query.Update
import org.springframework.data.mongodb.core.query.and
import org.springframework.data.mongodb.core.query.elemMatch
import org.springframework.data.mongodb.core.query.inValues
import org.springframework.data.mongodb.core.query.isEqualTo
import org.springframework.data.mongodb.core.query.regex
import org.springframework.data.mongodb.core.query.where
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.regex.Pattern

/**
 * 仓库服务实现类
 */
@Service
@Conditional(DefaultCondition::class)
class ProjectServiceImpl(
    private val projectDao: ProjectDao,
    private val servicePermissionClient: ServicePermissionClient,
    private val projectMetricsRepository: ProjectMetricsRepository,
    private val serviceBkiamV3ResourceClient: ServiceBkiamV3ResourceClient,
    private val storageCredentialService: StorageCredentialService,
) : ProjectService {

    @Autowired
    @Lazy
    private lateinit var resourcePermissionListener: ResourcePermissionListener

    override fun getProjectInfo(name: String): ProjectInfo? {
        return convert(projectDao.findByName(name))
    }

    override fun getProjectInfoByDisplayName(displayName: String): ProjectInfo? {
        return convert(projectDao.findByDisplayName(displayName))
    }

    override fun listProject(): List<ProjectInfo> {
        val criteria1 = TProject::name.regex("^$CLOSED_SOURCE_PREFIX")
        val criteria2 = TProject::name.regex("^$CODE_PROJECT_PREFIX")
        val query = Query().addCriteria(Criteria().norOperator(criteria1, criteria2)).limit(10000)
        return projectDao.find(query).map { convert(it)!! }
    }

    override fun searchProject(option: ProjectSearchOption): Page<ProjectInfo> {
        with(option) {
            val pageRequest = Pages.ofRequest(pageNumber, pageSize)
            val query = Query().with(pageRequest)

            if (!namePrefix.isNullOrEmpty()) {
                val criteria = TProject::name.regex("^${EscapeUtils.escapeRegex(namePrefix!!)}", "i")
                query.addCriteria(criteria)
            }

            val records = projectDao.find(query)
            return Pages.ofResponse(pageRequest, TOTAL_RECORDS_INFINITY, records.map { convert(it)!! })
        }
    }

    override fun listPermissionProject(userId: String, option: ProjectListOption?): List<ProjectInfo> {
        var names = servicePermissionClient.listPermissionProject(userId).data.orEmpty()
        option?.names?.let { names = names.intersect(option.names!!).toList() }
        val query = Query.query(
            where(TProject::name).`in`(names)
                .apply { option?.displayNames?.let { and(TProject::displayName).`in`(option.displayNames!!) } }
        )
        if (option?.sortProperty?.isNotEmpty() == true) {
            checkPropertyAndDirection(option)
            option.direction?.zip(option.sortProperty!!)?.forEach {
                query.with(Sort.by(Sort.Direction.valueOf(it.first), it.second))
            }
        }
        val projectList = if (option?.pageNumber == null && option?.pageSize == null) {
            projectDao.find(query)
        } else {
            val pageRequest = Pages.ofRequest(
                option.pageNumber ?: DEFAULT_PAGE_NUMBER,
                option.pageSize ?: DEFAULT_PAGE_SIZE
            )
            projectDao.find(query.with(pageRequest))
        }
        val projectIdList = projectList.map { it.name }
        val existProjectMap = serviceBkiamV3ResourceClient.getExistRbacDefaultGroupProjectIds(projectIdList).data
        return projectList.map {
            val exist = existProjectMap?.get(it.name) ?: false
            convert(it, exist)!!
        }
    }

    private fun checkPropertyAndDirection(option: ProjectListOption) {
        Preconditions.checkArgument(
            option.sortProperty?.none { !TProject::class.java.declaredFields.map { f -> f.name }.contains(it) },
            "sortProperty"
        )
        Preconditions.checkArgument(
            option.direction?.none { it != Sort.Direction.DESC.name && it != Sort.Direction.ASC.name },
            "direction"
        )
    }

    override fun rangeQuery(request: ProjectRangeQueryRequest): Page<ProjectInfo?> {
        val limit = request.limit
        val skip = request.offset
        var criteria = if (request.projectIds.isEmpty()) {
            Criteria()
        } else {
            TProject::name.inValues(request.projectIds)
        }
        if (request.projectMetadata.isNotEmpty()) {
            val metadataCriteria = request.projectMetadata.map {
                TProject::metadata.elemMatch(
                    ProjectMetadata::key.isEqualTo(it.key).and(ProjectMetadata::value.name).isEqualTo(it.value)
                )
            }
            criteria = Criteria().andOperator(metadataCriteria + criteria)
        }
        val query = Query(criteria)
        val totalCount = projectDao.count(query)
        val records = projectDao.find(query.limit(limit).skip(skip)).map { convert(it) }
        return Page(0, limit, totalCount, records)
    }

    override fun checkExist(name: String): Boolean {
        return projectDao.findByName(name) != null
    }

    override fun createProject(request: ProjectCreateRequest): ProjectInfo {
        with(request) {
            validateParameter(this)
            if (checkExist(name)) {
                throw ErrorCodeException(ArtifactMessageCode.PROJECT_EXISTED, name)
            }
            val project = TProject(
                name = name,
                displayName = displayName,
                description = description.orEmpty(),
                createdBy = operator,
                createdDate = LocalDateTime.now(),
                lastModifiedBy = operator,
                lastModifiedDate = LocalDateTime.now(),
                metadata = metadata,
                credentialsKey = credentialsKey,
            )
            return try {
                projectDao.insert(project)
                resourcePermissionListener.handle(buildCreatedEvent(request))
                logger.info("Create project [$name] success.")
                convert(project)!!
            } catch (exception: DuplicateKeyException) {
                logger.warn("Insert project[$name] error: [${exception.message}]")
                getProjectInfo(name)!!
            }
        }
    }

    /**
     * true:资源已存在, false: 资源不存在
     */
    override fun checkProjectExist(name: String?, displayName: String?): Boolean {
        val nameResult = name?.let { getProjectInfo(it) != null } ?: false
        val displayNameResult = displayName?.let { getProjectInfoByDisplayName(it) != null } ?: false
        return nameResult || displayNameResult
    }

    override fun getProjectMetricsInfo(name: String): ProjectMetricsInfo? {
        return convert(projectMetricsRepository.findFirstByProjectIdOrderByCreatedDateDesc(name))
    }

    override fun updateProject(name: String, request: ProjectUpdateRequest): Boolean {
        if (!checkExist(name)) {
            throw ErrorCodeException(ArtifactMessageCode.PROJECT_NOT_FOUND, name)
        }
        request.displayName?.let {
            if (it.length < DISPLAY_NAME_LENGTH_MIN || it.length > DISPLAY_NAME_LENGTH_MAX) {
                throw ErrorCodeException(CommonMessageCode.PARAMETER_INVALID, request::displayName.name)
            }
        }
        request.credentialsKey?.let { checkCredentialsKey(it) }
        val query = Query.query(Criteria.where(TProject::name.name).`is`(name))
        val update = Update().apply {
            request.displayName?.let { this.set(TProject::displayName.name, it) }
            request.description?.let { this.set(TProject::description.name, it) }
        }
        if (request.credentialsKey != null) {
            update.set(TProject::credentialsKey.name, request.credentialsKey)
        } else if (request.useDefaultCredentialsKey == true) {
            update.set(TProject::credentialsKey.name, null)
        }

        if (request.metadata.isNotEmpty()) {
            // 直接使用request的metadata，不存在于request的metadata会被删除，存在的会被覆盖
            update.set(TProject::metadata.name, request.metadata)
        }
        val updateResult = projectDao.updateFirst(query, update)
        return if (updateResult.modifiedCount == 1L) {
            logger.info("Update project [$name] success.")
            true
        } else {
            logger.error("Update project fail : $request")
            false
        }
    }

    private fun validateParameter(request: ProjectCreateRequest) {
        with(request) {
            if (!Pattern.matches(PROJECT_NAME_PATTERN, name)) {
                throw ErrorCodeException(CommonMessageCode.PARAMETER_INVALID, request::name.name)
            }
            if (displayName.isBlank() ||
                displayName.length < DISPLAY_NAME_LENGTH_MIN ||
                displayName.length > DISPLAY_NAME_LENGTH_MAX
            ) {
                throw ErrorCodeException(CommonMessageCode.PARAMETER_INVALID, request::displayName.name)
            }
            credentialsKey?.let { checkCredentialsKey(it) }
        }
    }

    private fun checkCredentialsKey(key: String) {
        storageCredentialService.findByKey(key) ?: throw ErrorCodeException(CommonMessageCode.RESOURCE_NOT_FOUND, key)
    }

    companion object {
        private val logger = LoggerFactory.getLogger(ProjectServiceImpl::class.java)
        private const val PROJECT_NAME_PATTERN = "[a-zA-Z_][a-zA-Z0-9\\-_]{1,99}"
        private const val DISPLAY_NAME_LENGTH_MIN = 2
        private const val DISPLAY_NAME_LENGTH_MAX = 100

        private fun convert(tProject: TProject?): ProjectInfo? {
            return convert(tProject, false)
        }

        private fun convert(tProject: TProject?, rbacFlag: Boolean): ProjectInfo? {
            return tProject?.let {
                ProjectInfo(
                    name = it.name,
                    displayName = it.displayName,
                    description = it.description,
                    createdBy = it.createdBy,
                    createdDate = it.createdDate.format(DateTimeFormatter.ISO_DATE_TIME),
                    lastModifiedBy = it.lastModifiedBy,
                    lastModifiedDate = it.lastModifiedDate.format(DateTimeFormatter.ISO_DATE_TIME),
                    metadata = it.metadata,
                    credentialsKey = it.credentialsKey,
                )
            }
        }

        private fun convert(tProjectMetrics: TProjectMetrics?): ProjectMetricsInfo? {
            return tProjectMetrics?.let {
                ProjectMetricsInfo(
                    projectId = it.projectId,
                    nodeNum = it.nodeNum,
                    capSize = it.capSize,
                    repoMetrics = it.repoMetrics,
                    createdDate = it.createdDate
                )
            }
        }
    }
}
